10.1 精通自定义 View 之 Android 画布——ShapeDrawable

返回自定义 View 目录

前面,我们提到了获取画布的几种方法。除了重写系统的 onDraw()、dispatchDraw() 函数,还可以通过以下方法获得画布:

  • 通过 Bitmap 创建。
  • 通过 SurfaceView 的 SurfaceHolder.lockCanvas() 函数获取。

另外,我们也提到过通过创建 Drawable 对象,然后将画好的 Drawable 对象画在画布上,也是创建 Bitmap 的一种方式。

Drawable 类有很多的派生类,如下图所示。

这些派生类都可以通过 Drawable 的 draw(Canvas canvas) 函数将其画到画布上。这里以最常用的 ShapeDrawable 为例来进行讲解。

shape 标签可以实现的效果与 ShapeDrawable 类似,但是 shape 标签所对应的 Java 类是 GradientDrawable,而不是 ShapeDrawable。使用如下代码会发生强转异常:

1
ShapeDrawable shapeDrawable = (ShapeDrawable) textView.getBackground();

10.1.1 shape 标签与 GradientDrawable

1. 是 ShapeDrawable 还是 GradientDrawable

前面讲过,shape 标签所对应的类是 GradientDrawable 而不是 ShapeDrawable,但是 GradientDrawable 并不能完成 shape 标签的所有功能,因为 GradientDrawable 的构造函数如下如所示。

从构造函数中可以明显看出,GradientDrawable 所对应的是 gradient 标签的功能,并不能完成 shape 标签所能完成的构造矩形、椭圆等功能;而神奇的是,通过 ShapeDrawable 却可以完成 shape 标签的所有功能!至于造成这种问题的原因,此处不再深究,只需知道在代码中得到 shape 标签实例的时候要强转 GradientDrawable 就可以了。

2. 获取 shape 标签的实例

实现这样一个功能:在单击按钮的时候,给原有的 shape 标签添加圆角。

新建 shape 文件:res/drawable/shape_solid.xml

1
2
3
4
5
6
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#FF0000"/>
<stroke android:width="2dp" android:color="#00FF00"
android:dashGap="5dp" android:dashWidth="5dp"/>
</shape>

在布局中使用:res/layout/act_main.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<Button
android:id="@+id/add_shape_corner"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="添加圆角"/>
<TextView
android:id="@+id/shape_tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="shape 标签实例"
android:padding="10dp"
android:layout_margin="20dp"
android:background="@drawable/shape_solid"/>
</LinearLayout>

动作代码:src/…/MainActivity.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class MainActivity extends AppCompatActivity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.act_main);
final TextView tv = findViewById(R.id.shape_tv);
findViewById(R.id.add_shape_corner).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
GradientDrawable drawable = (GradientDrawable) tv.getBackground();
drawable.setCornerRadius(20);
}
});
}
}

10.1.2 ShapeDrawable 的构造函数

ShapeDrawable 有两个构造函数:

1
2
3
4
5
6
// 一:无参构造函数,需配合 setShape()
ShapeDrawable()
setShape(Shape shape)
// 二:常用
ShapeDrawable(Shape shape)

Shape 是个抽象基类,实际应用中需要 Shape 的派生类。Shape 类的派生类如下图所示。

每个派生类的具体含义如下。

  • RectShape:构造一个矩形 Shape。
  • ArcShape:构造一个扇形 Shape。
  • OvalShape:构造一个椭圆 Shape。
  • RoundRectShape:构造一个圆角矩形 Shape,可带有镂空矩形效果。
  • PathShape:构造一个可根据路径绘制的 Shape。

1. RectShape

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class TestView extends View {
private ShapeDrawable mDrawable;
public TestView(Context context) {
super(context);
init();
}
public TestView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();
}
public TestView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
setLayerType(LAYER_TYPE_SOFTWARE, null);
mDrawable = new ShapeDrawable(new RectShape());
mDrawable.setBounds(new Rect(50, 50, 200, 100));
mDrawable.getPaint().setColor(Color.RED);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mDrawable.draw(canvas);
}
}

使用定义好的自定义控件:

1
2
3
4
5
6
7
8
9
10
11
12
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="#EEE">
<com.xxt.xtest.TestView
android:layout_width="100dp"
android:layout_height="80dp"
android:layout_margin="50dp"
android:background="#FFFFFF"/>
</LinearLayout>

效果图如下所示。

为了方便显示,将整个控件设置为白色。而且为了确认 mDrawable.setBounds(new Rect(50, 50, 200, 100)); 中矩形位置是在当前控件中的位置,我们给 TestView 控件添加了 margin 值。

从效果图中可以看出:

  • ShapeDrawable.setBounds() 函数所设置的矩形位置是指所在控件中的位置,而不是以屏幕左上角点为坐标的。
  • 通过 mDrawable.getPaint() 函数得到 ShapeDrawable 自带的画笔,并对其进行操作,效果将直接显示在 ShapeDrawable 中。

2. OvalShape

OvalShape 是指根据 ShapeDrawable.setBounds() 函数所定义的位置矩形生成一个椭圆形状的 Shape。

1
2
3
4
5
6
private void init() {
setLayerType(LAYER_TYPE_SOFTWARE, null);
mDrawable = new ShapeDrawable(new OvalShape());
mDrawable.setBounds(new Rect(50, 50, 200, 100));
mDrawable.getPaint().setColor(Color.RED);
}

这里的矩形位置和大小与 RectShape 中的矩形位置和大小一样,只是把 RectShape 改成了 OvalShape,效果如下图所示。

3. ArcShape

ArcShape 是在 OvalShape 所形成的椭圆的基础上,将其进行角度切割所形成的扇形。其中扇形开始的 0° 在椭圆的 X 轴正方向上。其只有一个构造函数。

1
public ArcShape(float startAngle, float sweepAngle)

  • startAngle:指开始角度,扇形开始的 0° 在椭圆的 X 轴正方向上,即右中间位置。
  • sweepAngle:指扇形所扫过的角度。

同举一个例子:

1
2
3
4
5
6
private void init() {
setLayerType(LAYER_TYPE_SOFTWARE, null);
mDrawable = new ShapeDrawable(new ArcShape(0, 300));
mDrawable.setBounds(new Rect(50, 50, 200, 100));
mDrawable.getPaint().setColor(Color.RED);
}

效果如下图所示。

4. RoundRectShape

RoundRectShape 在字面意思上是指圆角矩形。其实,它不仅能实现圆角矩形,它的本意是实现镂空的圆角矩形。它所能实现的效果如下图所示。

左图带有圆角的矩形,右图为中间带有镂空矩形的圆角矩形,而且中间的镂空矩形也可以带有圆角。

其构造函数如下:

1
public RoundRectShape(float[] outerRadii, RectF inset, float[] innerRadii)

  • float[] outerRadii:外围矩形的各个角的角度大小,需要填充 8 个数字,每两个数字一组,分别对应(左上角、右上角、右下角、左下角)4 个角的角度。每两个一组的数字构成一个椭圆,第一个数字代表椭圆的 X 轴半径,第二个数字代表椭圆的 Y 轴半径。如果不需要制定外围矩形的各个角的角度,则可以传入 null。
  • RectF inset:表示内部矩形与外部矩形各边的边距。RectF 的 4 个值分别对应 left、top、right、bottom 4 条边的边距。如果不需要内部矩形的镂空效果,则可以传入 null。
  • float[] innerRadii:表示内部矩形的各个角的角度大小,同样需要填充 8 个数字,其含义与 outerRadii 一样。如果不需要制定内部矩形的各个角的角度,则可以传入 null。

同样举一个例子:

1
2
3
4
5
6
7
8
9
private void init() {
setLayerType(LAYER_TYPE_SOFTWARE, null);
float[] outerR = new float[] { 12, 12, 12, 12, 0, 0, 0, 0 };
RectF inset = new RectF(6, 6, 6, 6);
float[] innerR = new float[] { 50, 12, 0, 0, 12, 50, 0, 0 };
mDrawable = new ShapeDrawable(new RoundRectShape(outerR, inset, innerR));
mDrawable.setBounds(new Rect(50, 50, 200, 100));
mDrawable.getPaint().setColor(Color.RED);
}

效果如下图所示。

5. PathShape

PathShape 的含义是构造一个可根据路径绘制的 Shape。其构造函数如下:

1
public PathShape(Path path, float stdWidth, float stdHeight)

  • path:表示所要画的路径。
  • stdWidth:表示标准宽度,即将整个 ShapeDrawable 的宽度分成多少份。Path 中的 moveTo(x, y)、lineTo(x2, y2) 这些函数中的数值在这里其实都是以每一份的位置来计算的。当 ShapeDrawable 动态变大、变小时,每一份都会变大变小,而根据这些份的数值画出来的 Path 图形就会动态缩放。
  • stdHight:表示标准高度,即将 ShapeDrawable 的高度分成多少份。

举例说明:

1
2
3
4
5
6
7
8
9
10
11
12
private void init() {
setLayerType(LAYER_TYPE_SOFTWARE, null);
Path path = new Path();
path.moveTo(0, 0);
path.lineTo(100, 0);
path.lineTo(100, 100);
path.lineTo(0, 100);
path.close();
mDrawable = new ShapeDrawable(new PathShape(path, 100, 100));
mDrawable.setBounds(new Rect(0, 0, 250, 150));
mDrawable.getPaint().setColor(Color.RED);
}

效果如下图所示:

为了验证 PathShape 份的概念,将 ShapeDrawable 的高度和宽度都分成了100 份。现在把高度的份数改成 200,那么,同样的路径代码,画出来的效果应该是高度的一半。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
private void init() {
setLayerType(LAYER_TYPE_SOFTWARE, null);
Path path = new Path();
path.moveTo(0, 0);
path.lineTo(100, 0);
path.lineTo(100, 100);
path.lineTo(0, 100);
path.close();
mDrawable = new ShapeDrawable(new PathShape(path, 100, 200));
mDrawable.setBounds(new Rect(0, 0, 250, 150));
mDrawable.getPaint().setColor(Color.RED);
}

效果如下图所示。

与 100 份的效果图对比,果然只占了一半。

6. 自定义 Shape

各个 Shape 派生类只不过实现了 Shape 中的 draw 函数。现在自定义实现一个构造区域的 Shape,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class RegionShape extends Shape {
private Region mRegion;
public RegionShape(Region region) {
assert(region != null);
mRegion = region;
}
@Override
public void draw(Canvas canvas, Paint paint) {
RegionIterator iterator = new RegionIterator(mRegion);
Rect rect = new Rect();
while (iterator.next(rect)) {
canvas.drawRect(rect, paint);
}
}
}

在 src/…/TestView.java 中使用 RegionShape:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public class TestView extends View {
private ShapeDrawable mDrawable;
public TestView(Context context) {
super(context);
init();
}
public TestView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();
}
public TestView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
setLayerType(LAYER_TYPE_SOFTWARE, null);
Rect rect1 = new Rect(50, 0, 90, 150);
Rect rect2 = new Rect(0, 50, 250, 100);
Region region1 = new Region(rect1);
Region region2 = new Region(rect2);
region1.op(region2, Region.Op.XOR);
mDrawable = new ShapeDrawable(new RegionShape(region1));
mDrawable.setBounds(new Rect(0, 0, 250, 150));
mDrawable.getPaint().setColor(Color.RED);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mDrawable.draw(canvas);
}
}

在 res/layout/act_main.xml 中使用 TestView:

1
2
3
4
5
6
7
8
9
10
11
12
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="#EEE">
<com.xxt.xtest.TestView
android:layout_width="250px"
android:layout_height="150px"
android:layout_margin="50dp"
android:background="#FFFFFF"/>
</LinearLayout>

效果如下图所示。

由于自定义 Shape 太过麻烦,一般不这么做。当我们需要使用 ShapeDrawable 无法完成的功能时,一般会通过自定义 Drawable 来实现。见 10.1.4 节。

10.1.3 常用函数

1. setBounds()

1
2
setBounds(int left, int top, int right, int bottom)
setBounds(Rect bounds)

它用来指定当前 ShapeDrawable 在当前控件中的显示位置。

2. getPaint()

1)概述

通过 ShapeDrawable.getPaint() 函数得到 ShapeDrawable 的 Paint 对象,并对其进行操作,效果就会立刻显示在 ShapeDrawable 上。这也意味着可以调用 Paint 中的所有函数;在自定义 Shape 时,可以调用 Canvas 的所有绘图方法。所以,ShapeDrawable 可以调用 Paint 和 Canvas 的所有方法,实现绘图的所有功能。

需要注意的地方:当 ShapeDrawable 的 Paint 调用 Shader 时,Shader 是从 ShapeDrawable 所在区域的左上角开始绘制的。

2)Paint.setShader()

下面举一个例子来证明我们的观点:Shader 是从 ShapeDrawable 所在区域的左上角开始绘制的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public class TestView extends View {
private ShapeDrawable mDrawable;
public TestView(Context context) {
super(context);
init();
}
public TestView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();
}
public TestView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
setLayerType(LAYER_TYPE_SOFTWARE, null);
mDrawable = new ShapeDrawable(new RectShape());
mDrawable.setBounds(new Rect(100, 100, 500, 500));
BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = 2;
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.head, options);
BitmapShader bitmapShader = new BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
mDrawable.getPaint().setShader(bitmapShader);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mDrawable.draw(canvas);
}
}

从效果图中可以看出,头像是在 TestView 控件的 Rect(100, 100, 500, 500) 位置绘制的,并不是从 TestView 的左上角开始绘制的,也不是从屏幕左上角开始绘制的。

3. 其他函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 设置透明度
setAlpha(int alpha)
// 设置 ColorFilter,是 ShapeDrawable 自带的函数
setColorFilter(ColorFilter colorFilter)
// 设置默认高度。当 Drawable 以 setBackgroundDrawable 及 setImageDrawable
// 方式使用时,会使用默认宽度和高度来计算当前 Drawable 的大小和位置。
// 如果不设置,则默认的宽高都是 -1px。详情请参考 10.1.4 节。
setIntrinsicHeight(int height)
// 设置默认宽度
setIntrinsicWidth(int width)
// 设置边距
setPadding(Rect padding)

4. 放大镜效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
public class TestView extends View {
private static final int RADIUS = 200; // 放大镜的半径
private static final int FACTOR = 3; // 放大倍数
private ShapeDrawable mDrawable;
private Bitmap mBitmap;
private final Matrix mMatrix = new Matrix();
public TestView(Context context) {
super(context);
init();
}
public TestView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();
}
public TestView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
setLayerType(LAYER_TYPE_SOFTWARE, null);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
final int x = (int) event.getX();
final int y = (int) event.getY();
// 这个位置表示的是绘制 Shader 的起始位置
mMatrix.setTranslate(RADIUS - x * FACTOR, RADIUS - y * FACTOR);
mDrawable.getPaint().getShader().setLocalMatrix(mMatrix);
mDrawable.setBounds(x - RADIUS, y - RADIUS, x + RADIUS, y + RADIUS);
invalidate();
return true;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (mBitmap == null) {
Bitmap bmp = BitmapFactory.decodeResource(getResources(), R.drawable.meinv);
mBitmap = Bitmap.createScaledBitmap(bmp, getWidth(), getHeight(), false);
Bitmap tempBmp = Bitmap.createScaledBitmap(mBitmap, mBitmap.getWidth() * FACTOR,
mBitmap.getHeight() * FACTOR, true);
BitmapShader shader = new BitmapShader(tempBmp, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
mDrawable = new ShapeDrawable(new OvalShape());
mDrawable.getPaint().setShader(shader);
mDrawable.setBounds(0, 0, RADIUS * 2, RADIUS * 2);
}
canvas.drawBitmap(mBitmap, 0, 0, null);
mDrawable.draw(canvas);
}
}

说明:

1)onDraw() 函数部分

之所以将初始化代码放在 onDraw() 函数中,是因为我们需要把图片缩放到控件大小。

1
2
Bitmap bmp = BitmapFactory.decodeResource(getResources(), R.drawable.meinv);
mBitmap = Bitmap.createScaledBitmap(bmp, getWidth(), getHeight(), false);

Bitmap.createScaledBitmap() 函数根据源图像生成一个指定宽度和高度的 Bitmap,这里就是指根据 bmp 创建一幅与当前控件同宽同高的图像,也就是将源图像缩放到当前控件的大小。

而 getWidth()、getHeight() 函数只有在调用 onLayout() 函数以后,这两个函数才能取到值的。

接下来创建 ShapeDrawable。

1
2
mDrawable = new ShapeDrawable(new OvalShape());
mDrawable.setBounds(0, 0, RADIUS * 2, RADIUS * 2);

这里创建一个椭圆形的 ShapeDrawable,而形成椭圆的矩形的宽高都是 RADIUS * 2,所以所形成的图形必然是一个圆形,且半径为 RADIUS。

最后是设置 BitmapShader 的过程。

1
2
3
4
5
Bitmap tempBmp = Bitmap.createScaledBitmap(mBitmap,
mBitmap.getWidth() * FACTOR, mBitmap.getHeight() * FACTOR, true);
BitmapShader shader = new BitmapShader(tempBmp, Shader.TileMode.CLAMP,
Shader.TileMode.CLAMP);
mDrawable.getPaint().setShader(shader);

同样使用 Bitmap.createScaledBitmap() 函数创建一张放大 3 倍的图片。

2)onTouchEvent() 函数部分

当手指有动作的时候,我们应当改变当前 ShapeDrawable 的显示位置。

1
mDrawable.setBounds(x - RADIUS, y - RADIUS, x + RADIUS, y + RADIUS);

即以当前手指位置为中心,画一个圆。

最关键的是 Shader 如何移动到我们要显示的位置。我们讲过,Shader 的开始显示位置在 ShapeDrawable 的左上角。所以,如果我们不移动 Shape,那么显示出来的永远是图片的左上角部分。那如何将 Shader 移动到图片的对应点呢?

我们需要先找到当前手指位置放大 3 倍的图片上的对应点,然后以这个对应点为中心显示出半径为 RADIUS 的圆中的图形。

当前手指的位置是 (x, y),那么放大 3 倍的图片上的对应点就是 (3x, 3y)。为了显示以放大 3 倍后的手指位置为中心的圆形区域,BitmapShader 需要向左和向上各移动多少呢?

首先,Shader 是从 ShapeDrawable 的左上角开始平铺的。也就是说,在初始状态下,ShapeDrawable 区域左上角一直显示的是 BitmapShader 的左上角(0, 0) 位置。我们在这里需要把 BitmapShader 向左上移动一段距离,以使 BitmapShader 中原来的 (3x, 3y) 点在 ShapeDrawable 区域中心。

第一步:我们可以将整个 BitmapShader 向左上移动 3x, 3y 的距离。由于在移动时,向右和向下是正值,所以左上移动的距离是 (-3x, -3y),而移动后的 BitmapShader 左上角显示的是 (3x, 3y) 处的图像。

第二步:我们需要将左上角点显示的 (3x, 3y) 处的图像显示在 ShapeDrawable 区域中心,所以需要将原本在左上角 (3x, 3y) 点在向右下移动一个半径的距离。所以总移动的距离为 (-3x+RADIUS, -3y + RADIUS)。

即:

1
2
mMatrix.setTranslate(RADIUS - x * FACTOR, RADIUS - y * FACTOR);
mDrawable.getPaint().getShader().setLocalMatrix(mMatrix);

10.1.4 自定义 Drawable

在 Drawable 的子类无法通过已有的函数完成指定的绘图功能时,一般会选择自定义 Drawable 来实现。本节将通过自定义 Drawable 来实现圆角功能。

1. 概述

我们写一个类,继承自 Drawable,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class TestDrawable extends Drawable {
@Override
public void draw(@NonNull Canvas canvas) {
}
@Override
public void setAlpha(int alpha) {
}
@Override
public void setColorFilter(@Nullable ColorFilter colorFilter) {
}
@Override
public int getOpacity() {
return PixelFormat.TRANSLUCENT;
}
}

这 4 个函数时 Drawable 类里的虚函数,是必须实现的。

  • draw() 函数是我们将会用到的,与 View 类似,传入的参数是一个 Canvas 对象,我们只需要调用 Canvas 的一些地方,效果就会直接显示在 Drawable 上。
  • setAlpha() 和 setColorFilter() 函数是非常容易实现的。当外层调用 TestDrawable 的这两个函数时,我们只需将对应的参数传给 TestDrawable 的 Paint 即可。
  • getOpacity():当外部需要知道我们自定义的 TestDrawable 的显示模式时会调用这个函数。它有 4 个取值:PixelFormat.UNKNOWN,TRANSLUCENT,TRANSPARENT,OPAQUE。其中,PixelFormat.TRANSLUCENT 表示当前 TestDrawable 的绘图是具有 Alpha 通道的,即使用 TestDrawable 后,其底部的图像仍有可能看到;PixelFormat.TRANSPARENT 表示当前 TestDrawable 是完全透明的,其中什么都没画,如果使用 TestDrawable,则将完全显示其底部图像;PixelFormat.OPAQUE 表示当前的 TestDrawable 是完全没有 Alpha 通道的,使用 TestDrawable 后,其底部的图像将被完全覆盖,而只显示 TestDrawable 本身的图像;PixelFormat.UNKNOWN 表示未知。一般而言,如果我们不知道该如何返回,则直接返回 PixelFormat.TRANSLUCENT 是最靠谱的做法。

2. 实现圆角 Drawable

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
public class TestDrawable extends Drawable {
private Paint mPaint;
private Bitmap mBitmap;
private BitmapShader mBitmapShader;
private RectF mBound;
public TestDrawable(Bitmap bitmap) {
mBitmap = bitmap;
mPaint = new Paint();
mPaint.setAntiAlias(true);
}
@Override
public void draw(@NonNull Canvas canvas) {
canvas.drawRoundRect(mBound, 20, 20, mPaint);
}
@Override
public void setAlpha(int alpha) {
mPaint.setAlpha(alpha);
}
@Override
public void setColorFilter(@Nullable ColorFilter colorFilter) {
mPaint.setColorFilter(colorFilter);
}
@Override
public int getOpacity() {
// 是否具有透明度是由传入的 Bitmap 所决定的。
return PixelFormat.TRANSLUCENT;
}
/**
* 根据边界创建一个与 Drawable 相同大小的 Bitmap 作为 Drawable 的 Shader
*/
@Override
public void setBounds(int left, int top, int right, int bottom) {
super.setBounds(left, top, right, bottom);
mBitmapShader = new BitmapShader(
Bitmap.createScaledBitmap(mBitmap, right - left, bottom - top, true),
Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
mPaint.setShader(mBitmapShader);
mBound = new RectF(left, top, right, bottom);
}
/**
* 设置 TestDrawable 的默认宽度
* @return int
*/
@Override
public int getIntrinsicWidth() {
return mBitmap.getWidth();
}
/**
* 设置 TestDrawable 的默认高度
* @return int
*/
@Override
public int getIntrinsicHeight() {
return mBitmap.getHeight();
}
}

3. Drawable 的使用方法

一般有两种使用方法:一种是通过 ImageView 的 setImageDrawable(drawable) 函数将其设置为 ImageView 的源图片;另一种是通过 View 的 setBackgroundDrawable(drawable) 函数将其设置为背景。

1)setImageDrawable(drawable) 函数

先在布局中定义一个 ImageView 标签

1
2
3
4
5
6
7
<ImageView
android:id="@+id/img"
android:layout_width="100dp"
android:layout_height="50dp"
android:layout_margin="10dp"
android:background="#FFFFFF"
android:scaleType="center"/>

在代码中使用

1
2
3
4
5
6
7
8
9
10
11
12
13
public class MainActivity extends AppCompatActivity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.act_main);
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.head);
ImageView imageView = findViewById(R.id.img);
TestDrawable drawable = new TestDrawable(bitmap);
imageView.setImageDrawable(drawable);
}
}

效果如下图所示。

可以看到,我们虽然在 TestDrawable 的 setBounds() 函数中将 Bitmap 缩放为整个边界大小,但是并没有覆盖整个 Bitmap,这是为什么呢?

在这里,我们使用 setImageDrawable(drawable) 函数来设置数据源,而源图片的显示大小是与 ImageView 的 scaleType 相关的。因为这里设置 scaleType=”center”,所以 ImageView 必然会居中缩放图片,然后将图片的显示位置通过 setBounds() 函数设置给 TestDrawable。下图展示了当前 Drawable 在不同的 scaleType 模式下的效果图。

布局如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="#EEE">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<ImageView
android:id="@+id/img"
android:layout_width="100dp"
android:layout_height="50dp"
android:layout_margin="10dp"
android:background="#FFFFFF"
android:scaleType="fitStart"/>
<ImageView
android:id="@+id/img2"
android:layout_width="100dp"
android:layout_height="50dp"
android:layout_margin="10dp"
android:background="#FFFFFF"
android:scaleType="fitCenter"/>
<ImageView
android:id="@+id/img3"
android:layout_width="100dp"
android:layout_height="50dp"
android:layout_margin="10dp"
android:background="#FFFFFF"
android:scaleType="fitEnd"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<ImageView
android:id="@+id/img4"
android:layout_width="100dp"
android:layout_height="50dp"
android:layout_margin="10dp"
android:background="#FFFFFF"
android:scaleType="centerInside"/>
<ImageView
android:id="@+id/img5"
android:layout_width="100dp"
android:layout_height="50dp"
android:layout_margin="10dp"
android:background="#FFFFFF"
android:scaleType="centerCrop"/>
<ImageView
android:id="@+id/img6"
android:layout_width="100dp"
android:layout_height="50dp"
android:layout_margin="10dp"
android:background="#FFFFFF"
android:scaleType="center"/>
</LinearLayout>
<ImageView
android:id="@+id/img7"
android:layout_width="100dp"
android:layout_height="50dp"
android:layout_margin="10dp"
android:background="#FFFFFF"
android:scaleType="fitXY"/>
</LinearLayout>

2)setBackgroundDrawable(drawable) 函数

布局:

1
2
3
4
5
6
7
<TextView
android:id="@+id/tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:text="欢迎光临先先生的 blog"
android:textColor="#FF0000"/>

代码:

1
2
3
4
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.head);
TestDrawable drawable = new TestDrawable(bitmap);
TextView tv = findViewById(R.id.tv);
tv.setBackgroundDrawable(drawable);

效果:

从效果图中可以看出,宽度使用的是 TextView 的宽度,而高度则使用的是 TestDrawable 的默认高度。之所以会出现这样的效果,是因为在使用 setBackgroundDrawable() 函数设置自定义 Drawable 时,自定义 Drawable 的宽度和高度计算是将 View 的宽、高和自定义 Drawable 的宽、高进行对比,哪个值大就用哪个值作为控件的宽、高的。而这个最终值就会通过 setBounds() 函数传递给自定义 Drawable。

4. 自定义 Drawable 与自定义 View 的区别

自定义 Drawable 的使用场景很明确,要么使用在可以设置 Drawable 的函数中(比如 setImageDrawable() 等),要么替代 Bitmap 用于 View 中(比如放大镜效果)。

而自定义 View 的功能十分强大,自定义 Drawable 和 Bitmap 无法完成的功能可以使用自定义 View 来完成。

10.1.5 Drawable 与 Bitmap 的对比

1. 定义对比

Bitmap 称作位图,一般位图的文件格式扩展名为 .bmp,当然编码器也有很多,如 RGB565、RGB8888。作为一种逐像素的显示对象,其执行效率高;但存储效率低。

Drawable 作为 Android 下通用的图形对象,它可以装载常用格式的图像,比如 GIF、PNG、JPG 和 BMP,还提供了一些高级的可视化对象,比如渐变、图形等。

也就是说,Bitmap 是 Drawable,而 Drawable 不一定是 Bitmap。

2. 指标对比

对比项 显示清晰度 占用内存 支持缩放 支持色相色差调整 支持旋转 支持透明色 绘制速度 支持像素操作
Bitmap 相同
Drawable 相同

3. 绘图便利性对比

Drawable 有很多派生类,通过这些派生类可以很容易地生成渐变、层叠等效果。单从这一方面而言,Drawable 比 Bitmap 有优势。

但如果仅仅用作空白画布来绘图,那么 Drawable 构造和使用起来则不如 Bitmap 方便。

4. 使用简易性对比

Drawable 子类是自带画笔,调用 Paint 的函数很方便。但使用 Canvas 的函数并不方便,所以 Drawable 的子类一般只用来完成它固有的功能。如果想要使用 Drawable 绘图,则建议自定义 Drawable。

而如果想在 Bitmap 上作画,则一般使用类似如下的代码:

1
2
3
4
Canvas canvas = new Canvas(bitmap);
Paint paint = new Paint();
paint.setColor(Color.RED);
canvas.drawCircle(0, 0, 100, paint);

从代码中可以看到,如果 Bitmap 想要作为画布,则需要通过 Canvas canvas = new Canvas(bitmap); 来创建 Canvas 对象,而通过生成的 Canvas 对象,所绘制的内容是直接画在 Bitmap 上的。而且画笔也是可以随意定义的。

所以,就使用简易性而言,Bitmap 确实要比 Drawable 易用。

5. 使用方式对比

Bitmap 主要靠在 View 中通过 Canvas.drawBitmap() 函数画出来;而 Drawable 不仅能在 View 中通过 Drawable.draw(Canvas canvas) 函数画出来,也可以通过 setImageBackground()、setBackgroundDrawable() 等设置 Drawable 资源的函数来设置。

总结:

  • Bitmap 在占用内存和绘制速度上不如 Drawable 有优势。
  • Bitmap 绘图方便。
  • Drawable 有一些子类,可以方便地完成一些绘图功能。

那么,Drawable、Bitmap、自定义 View 在哪些情况下使用呢?

  • Bitmap 只在一种情况下使用,即在 View 中需要自己生成图像时,才会使用 Bitmap 绘图。绘图后的结果保存在这个 Bitmap 中,供自己定义 View 使用。比如根据源 Bitmap 生成它的倒影,在使用 Xfermode 来融合倒转的图片原图与渐变的图片时,就需要根据图片大小生成一张同样大小的渐变图片,这时必须使用 Bitmap。
  • 当使用 Drawable 的子类完成一些固有功能时,优先选用 Drawable。
  • 当需要使用 setImageDrawable()、setBackgroundDrawable() 等可以直接设置 Drawable 资源的函数时,只能选用 Drawable。
  • 在自定义 View 中指定位置显示图形功能时,既可以使用 Drawable,也可以使用 Bitmap。
  • 除 Drawable 和 Bitmap 以外的地方,都可以使用自定义 View 来实现。